Skip to content

Conversation

@hyujikoh
Copy link
Owner

@hyujikoh hyujikoh commented Jan 2, 2026

📌 Summary

Spring Batch를 활용한 주간/월간 랭킹 집계 시스템을 구현하고, API 구조를 개선했습니다.

주요 구현 내용:

  • Batch Jobs (commerce-batch): Chunk-Oriented 방식으로 주간/월간 TOP 100 랭킹 집계
  • Materialized View: 조회 전용 테이블 설계 및 Entity 구현 (복합키 @EmbeddedId)
  • API 리팩토링 (commerce-api): 랭킹 전용 RankingV1Controller 분리 및 Java 8 Date API 활용
  • 핵심 비즈니스 로직: 점수 계산, 날짜 파싱, 집계 로직을 단위 테스트로 검증

구현된 배치 프로세스:

  • 주간 집계: product_metrics 7일치 → mv_product_rank_weekly TOP 100
  • 월간 집계: product_metrics 30일치 → mv_product_rank_monthly TOP 100
  • 점수 계산: viewCount×1 + likeCount×3 + salesCount×5 + orderCount×2

💬 Review Points

각 섹션별로 배경 → 해결 방안 → 구현 세부사항 → 고민한 점 순으로 정리했습니다.


1. Spring Batch Job 설계 (Chunk-Oriented Processing)

배경 및 문제 상황:
대량의 메트릭 데이터를 집계하여 주간/월간 랭킹을 생성해야 하는데, 메모리 효율성과 트랜잭션 안정성을 고려했습니다.

해결 방안:
Chunk-Oriented Processing 패턴을 적용하여 Reader → Processor → Writer 구조로 설계했습니다.

구현 세부사항:

1) Job Configuration - 파라미터 기반 동작:

// WeeklyRankingJobConfig.java
@ConditionalOnProperty(name = "spring.batch.job.name", havingValue = "weeklyRankingJob")
@Configuration
public class WeeklyRankingJobConfig {
    public static final String JOB_NAME = "weeklyRankingJob";
    private static final int CHUNK_SIZE = 100;  // TOP 100이므로 한 번에 처리
    
    @Bean(JOB_NAME)
    public Job weeklyRankingJob() {
        return new JobBuilder(JOB_NAME, jobRepository)
            .incrementer(new RunIdIncrementer())
            .start(weeklyAggregationStep())
            .listener(jobListener)
            .build();
    }
    
    @Bean("weeklyAggregationStep")
    @JobScope
    public Step weeklyAggregationStep() {
        return new StepBuilder("weeklyAggregationStep", jobRepository)
            .<RankingAggregation, RankingAggregation>chunk(CHUNK_SIZE, transactionManager)
            .reader(weeklyMetricsReader)
            .processor(rankingProcessor)
            .writer(weeklyRankWriter)
            .listener(stepMonitorListener)
            .build();
    }
}

2) ItemReader - DB 집계 쿼리 + TOP 100 필터링:

// WeeklyMetricsReader.java
@StepScope
@Component
public class WeeklyMetricsReader implements ItemReader<RankingAggregation> {
    
    @Value("#{jobParameters['yearWeek']}")
    private String yearWeek;  // e.g., "2024-W52"
    
    @Override
    public RankingAggregation read() {
        if (iterator == null) {
            // 1. 주차 → 날짜 범위 변환
            LocalDate[] dateRange = dateRangeParser.parseYearWeek(yearWeek);
            
            // 2. DB 집계 쿼리 실행
            List<Object[]> results = repository.aggregateByDateRange(
                dateRange[0], dateRange[1]);
            
            // 3. 집계 + 점수 계산 + 정렬 + TOP 100 + 순위 부여
            List<RankingAggregation> aggregations = rankingAggregator
                .aggregateAndRank(results, scoreCalculator, 100);
            
            iterator = aggregations.iterator();
        }
        return iterator.hasNext() ? iterator.next() : null;
    }
}

3) ItemWriter - 멱등성 보장:

// WeeklyRankWriter.java
@StepScope
@Component
public class WeeklyRankWriter implements ItemWriter<RankingAggregation> {
    
    @Value("#{jobParameters['yearWeek']}")
    private String yearWeek;
    
    @Override
    public void write(Chunk<? extends RankingAggregation> chunk) {
        List<WeeklyRankEntity> entities = chunk.getItems().stream()
            .map(agg -> WeeklyRankEntity.create(
                agg.getProductId(), yearWeek, agg.getViewCount(),
                agg.getLikeCount(), agg.getSalesCount(), agg.getOrderCount(),
                agg.getTotalScore(), agg.getRankPosition()))
            .toList();
            
        // 멱등성 보장: 기존 데이터 삭제 후 저장
        repository.deleteByYearWeek(yearWeek);
        repository.saveAll(entities);
    }
}

고민한 점:

  • Chunk Size를 100으로 설정한 이유: TOP 100만 처리하므로 한 번에 처리하여 트랜잭션 오버헤드를 최소화했습니다.
  • Reader에서 집계 처리한 이유: DB에서 GROUP BY로 집계한 후 애플리케이션에서 정렬/필터링하였습니다.
  • 멱등성 보장: 같은 기간에 재실행해도 동일한 결과를 보장하도록 기존 데이터 삭제 후 저장하는 방식을 채택했습니다.

2. 핵심 비즈니스 로직 모듈화 및 단위 테스트

  • 배경 및 문제 상황: 배치 Job의 복잡한 로직(점수 계산, 날짜 파싱, 집계 처리)을 통합 테스트로만 검증하면 실패 시 원인 파악이 어렵고, 테스트 실행 시간이 오래 걸릴것으로 예상했습니다.

  • 해결 방안: 핵심 비즈니스 로직을 별도 클래스로 분리하고 단위 테스트로 검증한 후, Job에 조립하는 방식을 채택했습니다.

  • 구현 세부사항:
    1) ScoreCalculator - 점수 계산 로직:

// ScoreCalculator.java

@Component
public class ScoreCalculator {
    private static final int VIEW_WEIGHT = 1;
    private static final int LIKE_WEIGHT = 3;
    private static final int SALES_WEIGHT = 5;
    private static final int ORDER_WEIGHT = 2;
    
    public long calculate(long viewCount, long likeCount, long salesCount, long orderCount) {
        return viewCount * VIEW_WEIGHT 
             + likeCount * LIKE_WEIGHT 
             + salesCount * SALES_WEIGHT 
             + orderCount * ORDER_WEIGHT;
    }
}

// ScoreCalculatorUnitTest.java
@Test
@DisplayName("가중치가 올바르게 적용되어 점수가 계산된다")
void should_calculate_score_with_correct_weights() {
    // given
    long viewCount = 100, likeCount = 50, salesCount = 10, orderCount = 5;
    
    // when
    long score = scoreCalculator.calculate(viewCount, likeCount, salesCount, orderCount);
    
    // then
    // 100*1 + 50*3 + 10*5 + 5*2 = 310
    assertThat(score).isEqualTo(310L);
}

2) DateRangeParser - 날짜 범위 파싱:

// DateRangeParser.java
@Component
public class DateRangeParser {
    
    public LocalDate[] parseYearWeek(String yearWeek) {
        // "2024-W52" → [2024-12-23, 2024-12-29]
        WeekFields weekFields = WeekFields.ISO;
        String[] parts = yearWeek.split("-W");
        int year = Integer.parseInt(parts[0]);
        int week = Integer.parseInt(parts[1]);
        
        LocalDate startOfWeek = LocalDate.of(year, 1, 1)
            .with(weekFields.weekOfYear(), week)
            .with(weekFields.dayOfWeek(), 1);
        LocalDate endOfWeek = startOfWeek.plusDays(6);
        
        return new LocalDate[]{startOfWeek, endOfWeek};
    }
}

// DateRangeParserUnitTest.java
@Test
@DisplayName("2024년 52주차를 올바른 날짜 범위로 파싱한다")
void should_parse_year_week_to_correct_date_range() {
    // when
    LocalDate[] result = dateRangeParser.parseYearWeek("2024-W52");
    
    // then
    assertThat(result[0]).isEqualTo(LocalDate.of(2024, 12, 23)); // 월요일
    assertThat(result[1]).isEqualTo(LocalDate.of(2024, 12, 29)); // 일요일
}

3) RankingAggregator - 집계 및 순위 부여:

// RankingAggregator.java
@Component
public class RankingAggregator {
    
    public List<RankingAggregation> aggregateAndRank(
            List<Object[]> dbResults, ScoreCalculator calculator, int topN) {
        
        List<RankingAggregation> aggregations = dbResults.stream()
            .map(row -> RankingAggregation.from(row, calculator))
            .sorted(Comparator.comparingLong(RankingAggregation::getTotalScore).reversed())
            .limit(topN)
            .toList();
        
        // 순위 부여
        for (int i = 0; i < aggregations.size(); i++) {
            aggregations.get(i).assignRank(i + 1);
        }
        
        return aggregations;
    }
}

// RankingAggregatorUnitTest.java
@Test
@DisplayName("점수 순으로 정렬하고 TOP N을 선택하여 순위를 부여한다")
void should_sort_by_score_and_assign_ranks_for_top_n() {
    // given
    List<Object[]> dbResults = List.of(
        new Object[]{1L, 100L, 50L, 10L, 5L},  // 점수: 310
        new Object[]{2L, 200L, 30L, 5L, 3L},   // 점수: 281
        new Object[]{3L, 50L, 100L, 20L, 10L}  // 점수: 470
    );
    
    // when
    List<RankingAggregation> result = aggregator.aggregateAndRank(dbResults, calculator, 2);
    
    // then
    assertThat(result).hasSize(2);
    assertThat(result.get(0).getProductId()).isEqualTo(3L);  // 1위: 470점
    assertThat(result.get(0).getRankPosition()).isEqualTo(1);
    assertThat(result.get(1).getProductId()).isEqualTo(1L);  // 2위: 310점
    assertThat(result.get(1).getRankPosition()).isEqualTo(2);
}

고민한 점:

  • 단위 테스트 우선 접근: 복잡한 배치 로직을 작은 단위로 분해하여 각각 단위 테스트로 검증한 후 조립하는 방식으로, 문제 발생 시 빠른 원인 파악이 가능하도록 했습니다.

  • 점수 계산 공식: Redis ZSET의 가중치와 동일하게 유지하여 일간/주간/월간 랭킹 간 일관성을 보장했습니다.

  • ISO Week 표준: WeekFields.ISO를 사용하여 국제 표준 주차 계산을 적용했습니다.

3. Materialized View 설계 (@EmbeddedId 복합키)

  • 배경 및 문제 상황: 주간/월간 랭킹 데이터를 효율적으로 저장하고 조회하기 위한 테이블 구조가 필요했습니다. 특히 (product_id, year_week) 또는 (product_id, year_month) 복합키로 유니크 제약을 보장해야 했습니다.

  • 해결 방안: Hibernate 6.x 권장 방식인 @EmbeddedId를 사용하여 복합키를 객체로 캡슐화했습니다.

구현 세부사항:

1) 복합키 클래스 (@embeddable):

// WeeklyRankId.java
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode  // 복합키는 equals/hashCode 필수
public class WeeklyRankId implements Serializable {
    
    @Column(name = "product_id", nullable = false)
    private Long productId;
    
    @Column(name = "year_week", nullable = false)
    private String yearWeek;
    
    public static WeeklyRankId of(Long productId, String yearWeek) {
        return new WeeklyRankId(productId, yearWeek);
    }
}
  1. Entity 클래스 (@EmbeddedId + 정적 팩토리 메서드):
// WeeklyRankEntity.java
@Entity
@Getter
@Table(
    name = "mv_product_rank_weekly",
    indexes = @Index(name = "idx_year_week_rank", columnList = "year_week, rank_position")
)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class WeeklyRankEntity {

    @EmbeddedId
    private WeeklyRankId id;

    @Column(name = "view_count", nullable = false)
    private long viewCount = 0L;
    
    @Column(name = "like_count", nullable = false)
    private long likeCount = 0L;
    
    @Column(name = "sales_count", nullable = false)
    private long salesCount = 0L;
    
    @Column(name = "order_count", nullable = false)
    private long orderCount = 0L;
    
    @Column(name = "total_score", nullable = false)
    private long totalScore = 0L;
    
    @Column(name = "rank_position", nullable = false)
    private int rankPosition;

    public static WeeklyRankEntity create(Long productId, String yearWeek,
            long viewCount, long likeCount, long salesCount, long orderCount,
            long totalScore, int rankPosition) {
        return new WeeklyRankEntity(productId, yearWeek, viewCount, likeCount,
                salesCount, orderCount, totalScore, rankPosition);
    }

    // 편의 메서드
    public Long getProductId() {
        return id.getProductId();
    }

    public String getYearWeek() {
        return id.getYearWeek();
    }
    
    private void validateRankPosition(int rankPosition) {
        if (rankPosition < 1 || rankPosition > 100) {
            throw new IllegalArgumentException(
                String.format("순위는 1~100 범위여야 합니다. (입력값: %d)", rankPosition));
        }
    }
}

고민한 점:

  • @EmbeddedId vs @IdClass: @EmbeddedId는 복합키를 객체로 캡슐화하여 타입 안전성을 제공하고, Hibernate 6.x에서 권장하는 방식을 이용해 구현하도록 했습니다.

  • 인덱스 설계: (year_week, rank_position) 복합 인덱스로 주차별 순위 조회를 최적화했습니다.

  • 순위 검증: Entity 생성 시 1~100 범위 검증으로 데이터 무결성을 보장했습니다.

4. Controller 리팩토링 - 관심사 분리

  • 배경 및 문제 상황: 기존 ProductV1Controller에 랭킹 관련 메서드가 포함되어 있어 관심사가 혼재되어 있었습니다. 또한 Java 8 Date API를 활용한 파라미터 처리가 필요했습니다.

  • 해결 방안: 랭킹 전용 RankingV1Controller를 분리하고, Java 8 Date API(WeekFields, YearMonth)를 활용하여 파라미터를 자동 처리하도록 구현했습니다.

구현 세부사항:

1) RankingV1Controller - 전용 컨트롤러:

// RankingV1Controller.java
@RestController
@RequiredArgsConstructor
public class RankingV1Controller implements RankingV1ApiSpec {

    private final ProductFacade productFacade;

    @GetMapping(Uris.Ranking.GET_RANKING)
    @Override
    public ApiResponse<PageResponse<ProductV1Dtos.ProductListResponse>> getRankingProducts(
            @PageableDefault(size = 20) Pageable pageable,
            @RequestParam(required = false) LocalDate date
    ) {
        Page<ProductInfo> products = productFacade.getRankingProducts(pageable, date);
        Page<ProductV1Dtos.ProductListResponse> responsePage = products.map(ProductV1Dtos.ProductListResponse::from);
        return ApiResponse.success(PageResponse.from(responsePage));
    }

    @GetMapping(Uris.Ranking.GET_RANKING_BY_PERIOD)
    @Override
    public ApiResponse<PageResponse<ProductV1Dtos.ProductListResponse>> getRankingProductsByPeriod(
            @RequestParam RankingPeriod period,
            @PageableDefault(size = 20) Pageable pageable,
            @RequestParam(required = false) LocalDate date,
            @RequestParam(required = false) String yearWeek,
            @RequestParam(required = false) String yearMonth
    ) {
        // Java 8 Date API를 활용한 파라미터 검증 및 변환
        String processedYearWeek = processYearWeekParameter(yearWeek, date);
        String processedYearMonth = processYearMonthParameter(yearMonth, date);

        Page<ProductInfo> products = productFacade.getRankingProductsByPeriod(
                period, pageable, date, processedYearWeek, processedYearMonth);
        Page<ProductV1Dtos.ProductListResponse> responsePage = products.map(ProductV1Dtos.ProductListResponse::from);
        return ApiResponse.success(PageResponse.from(responsePage));
    }

    /**
     * yearWeek 파라미터 처리
     * - 파라미터가 없으면 현재 주차로 설정
     */
    private String processYearWeekParameter(String yearWeek, LocalDate date) {
        if (yearWeek != null && !yearWeek.trim().isEmpty()) {
            return yearWeek;
        }

        LocalDate targetDate = date != null ? date : LocalDate.now();
        WeekFields weekFields = WeekFields.of(Locale.getDefault());
        int year = targetDate.getYear();
        int week = targetDate.get(weekFields.weekOfYear());
        
        return String.format("%d-W%02d", year, week);
    }

    /**
     * yearMonth 파라미터 처리
     * - 파라미터가 없으면 현재 월로 설정
     */
    private String processYearMonthParameter(String yearMonth, LocalDate date) {
        if (yearMonth != null && !yearMonth.trim().isEmpty()) {
            return yearMonth;
        }

        LocalDate targetDate = date != null ? date : LocalDate.now();
        YearMonth ym = YearMonth.from(targetDate);
        
        return ym.format(DateTimeFormatter.ofPattern("yyyy-MM"));
    }
}

2) ProductV1Controller - 랭킹 메서드 제거:

// ProductV1Controller.java (리팩토링 후)
@RestController
@RequiredArgsConstructor
public class ProductV1Controller implements ProductV1ApiSpec {

    private final ProductFacade productFacade;

    @GetMapping(Uris.Product.GET_LIST)
    @Override
    public ApiResponse<PageResponse<ProductV1Dtos.ProductListResponse>> getProducts(...) {
        // 상품 목록 조회 (기존 유지)
    }

    @GetMapping(Uris.Product.GET_DETAIL)
    @Override
    public ApiResponse<ProductV1Dtos.ProductDetailResponse> getProductDetail(...) {
        // 상품 상세 조회 (기존 유지)
    }
    
    // 랭킹 관련 메서드 제거됨 → RankingV1Controller로 이동
}

고민한 점:

  • 관심사 분리: 상품 조회와 랭킹 조회를 별도 컨트롤러로 분리하여 단일 책임 원칙을 준수하고자 했습니다.

5. E2E 테스트

  • 배경 및 구현 : 랭킹 e2e 테스트를 통해 기능 점검을 하였습니다.

구현 세부사항:

// RankingApiE2ETest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DisplayName("Ranking API E2E 테스트")
class RankingApiE2ETest {

    @Nested
    @DisplayName("랭킹 목록 조회 API")
    class GetRankingProductsTest {
        @Test
        @DisplayName("랭킹 데이터가 있으면 점수 순으로 상품 목록을 반환한다")
        void should_return_products_in_ranking_order() { ... }
        
        @Test
        @DisplayName("랭킹 데이터가 없으면 빈 목록을 반환한다")
        void should_return_empty_when_no_ranking_data() { ... }
        
        @Test
        @DisplayName("페이징이 정상적으로 동작한다")
        void should_paginate_ranking_results() { ... }
        
        @Test
        @DisplayName("특정 날짜의 랭킹을 조회할 수 있다")
        void should_return_ranking_for_specific_date() { ... }
    }

    @Nested
    @DisplayName("콜드 스타트 Fallback 테스트")
    class ColdStartFallbackTest {
        @Test
        @DisplayName("오늘 랭킹이 없으면 어제 랭킹을 반환한다")
        void should_fallback_to_yesterday_when_today_is_empty() { ... }
        
        @Test
        @DisplayName("명시적 날짜 지정 시 Fallback하지 않는다")
        void should_not_fallback_when_date_is_explicitly_specified() { ... }
    }

    @Nested
    @DisplayName("상품 상세 조회 시 랭킹 정보 포함 테스트")
    class ProductDetailWithRankingTest { ... }

    @Nested
    @DisplayName("점수 누적 테스트")
    class ScoreAccumulationTest { ... }

    @Nested
    @DisplayName("Score Carry-Over 테스트")
    class CarryOverTest { ... }
}

테스트 커버리지:

  • 랭킹 목록 조회 (4개 케이스)
  • 콜드 스타트 Fallback (2개 케이스)
  • 상품 상세 + 랭킹 정보 (3개 케이스)
  • 점수 누적 (1개 케이스)
  • Score Carry-Over (1개 케이스)
    => 총 11개 테스트 케이스 모두 통과

✅ Checklist

🧱 Spring Batch (3/3)

  • Spring Batch Job을 작성하고, 파라미터 기반으로 동작시킬 수 있다
    • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/WeeklyRankingJobConfig.java
    • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/MonthlyRankingJobConfig.java
    • Job 파라미터: yearWeek, yearMonth
  • Chunk Oriented Processing (Reader / Processor / Writer) 기반의 배치 처리를 구현했다
    • Reader: WeeklyMetricsReader.java, MonthlyMetricsReader.java
    • Processor: RankingProcessor.java (공통)
    • Writer: WeeklyRankWriter.java, MonthlyRankWriter.java
  • 집계 결과를 저장할 Materialized View의 구조를 설계하고 올바르게 적재했다
    • modules/jpa/src/main/java/com/loopers/domain/ranking/WeeklyRankEntity.java
    • modules/jpa/src/main/java/com/loopers/domain/ranking/MonthlyRankEntity.java
    • 복합키(@EmbeddedId) + TOP 100 + 순위 정보

🧩 Ranking API (1/1)

  • API가 일간, 주간, 월간 랭킹을 제공하며 조회 형태에 따라 적절한 데이터 기반으로 응답한다
    • 일간: GET /api/v1/rankings (Redis ZSET)
    • 주간/월간: GET /api/v1/rankings/period (MV 테이블)
    • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java

🧪 핵심 로직 단위 테스트 (4/4)

  • 점수 계산 로직 (ScoreCalculator) 검증
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/ScoreCalculatorUnitTest.java
  • 날짜 파싱 로직 (DateRangeParser) 검증
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/DateRangeParserUnitTest.java
  • 집계 및 순위 부여 로직 (RankingAggregator) 검증
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/RankingAggregatorUnitTest.java
  • DTO 생성 및 검증 로직 (RankingAggregation) 테스트
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/dto/RankingAggregationUnitTest.java

🔧 코드 품질

  • 컴파일 에러 해결
  • 코딩 가이드라인 준수 (DDD 레이어드 아키텍처)
  • 불필요한 코드 제거
  • JavaDoc 및 주석 보강
  • 테스트 파일 통합 및 중복 제거

🧪 E2E 테스트

  • 랭킹 API E2E 테스트 (11개 테스트 케이스)
    • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java
  • 배치 Job E2E 테스트
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/RankingBatchE2ETest.java
  • 수동 배치 실행 테스트
    • apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/ManualBatchJobTest.java

📎 References

주요 구현 파일

배치 시스템:

  • apps/commerce-batch/src/main/java/com/loopers/batch/job/ranking/
    • Job 설정: WeeklyRankingJobConfig.java, MonthlyRankingJobConfig.java
    • Reader: WeeklyMetricsReader.java, MonthlyMetricsReader.java
    • Writer: WeeklyRankWriter.java, MonthlyRankWriter.java
    • 핵심 로직: ScoreCalculator.java, DateRangeParser.java, RankingAggregator.java

API 시스템:

  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (리팩토링)

도메인 모델:

  • modules/jpa/src/main/java/com/loopers/domain/ranking/
    • WeeklyRankEntity.java, WeeklyRankId.java
    • MonthlyRankEntity.java, MonthlyRankId.java

테스트:

  • 단위 테스트: apps/commerce-batch/src/test/java/com/loopers/batch/job/ranking/support/
  • E2E 테스트: apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingApiE2ETest.java

기술 참고 자료

  • Spring Batch 공식 문서 - Chunk-Oriented Processing
  • Hibernate @EmbeddedId 가이드
  • Java 8 Date API - WeekFields

지금 이 pr 을 깔끔하게 code - block 을 정리해서 재 작성해줘

JVHE and others added 30 commits October 28, 2025 17:01
회원 가입시 User 저장이 수행된다. ( spy 검증 )
이미 가입된 ID 로 회원가입 시도 시, 실패한다.
회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.
회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.
해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.
해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
내 정보 조회에 성공할 경우, 해당하는 유저 정보를 응답으로 반환한다.
존재하지 않는 ID 로 조회할 경우, `404 Not Found` 응답을 반환한다.
해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.
해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.

포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.
`X-USER-ID` 헤더가 없을 경우, `400 Bad Request` 응답을 반환한다.
0 이하의 정수로 포인트를 충전 시 실패한다.
존재하지 않는 유저 ID 로 충전을 시도한 경우, 실패한다.
존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.
존재하지 않는 유저로 요청할 경우, `404 Not Found` 응답을 반환한다.
JVHE and others added 28 commits November 15, 2025 11:25
[volume-3] 도메인 모델링 및 구현
…9-revert-86-3round

Revert "Revert "[volume-3] 도메인 모델링 및 구현""
…-3round

Revert "[volume-3] 도메인 모델링 및 구현"
Round3: Product, Brand, Like, Order
…-round3

Revert "Round3: Product, Brand, Like, Order"
- 상품별 일간 메트릭을 저장하는 ProductMetricsEntity 추가
- 복합키를 사용하여 메트릭을 구분하는 ProductMetricsId 추가
- 상품 ID와 날짜로 메트릭을 조회하는 메서드 구현
- 메트릭 저장 및 조회 기능을 제공하는 ProductMetricsRepository 추가
- 메트릭 관련 비즈니스 로직을 처리하는 ProductMetricsService 추가
- 메트릭 엔티티에 대한 단위 테스트 추가
- 상품별 일간 메트릭을 저장하는 ProductMetricsEntity 추가
- 복합키를 사용하여 메트릭을 구분하는 ProductMetricsId 추가
- 상품 ID와 날짜로 메트릭을 조회하는 메서드 구현
- 메트릭 저장 및 조회 기능을 제공하는 ProductMetricsRepository 추가
- 메트릭 관련 비즈니스 로직을 처리하는 ProductMetricsService 추가
- 메트릭 엔티티에 대한 단위 테스트 추가
- 주간 및 월간 랭킹 집계 Job 설정
- 메트릭 데이터 처리기 및 점수 계산기 구현
- 랭킹 집계 및 저장 로직 추가
- 날짜 범위 파싱 유틸리티 구현
- 관련 테스트 코드 추가
- 월간 및 주간 랭킹 엔티티(MonthlyRankEntity, WeeklyRankEntity) 생성
- 각 엔티티에 대한 JPA Repository 및 서비스 구현
- 랭킹 데이터 저장 및 조회 기능 추가
- 관련 테스트 코드 작성
- rankPosition 필드 타입을 int에서 long으로 변경
- createdAt 필드 타입을 ZonedDateTime에서 LocalDateTime으로 변경
- 불필요한 메서드 주석 제거
- 일간, 주간, 월간 랭킹 조회 API 명세 및 구현
- 랭킹 데이터 처리 로직 추가
- 기존 코드 리팩토링 및 불필요한 메서드 제거
- rank_position을 base_rank_position으로 변경
- year_month를 base_year_month로 변경
- 로그 메시지 개선 및 예외 처리 강화
- 코드 가독성을 높이기 위해 불필요한 임포트 문을 제거
- 주석 및 공백 정리로 코드 일관성 향상
@hyujikoh hyujikoh self-assigned this Jan 2, 2026
@hyujikoh hyujikoh merged commit 78ead14 into week-10 Jan 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants